package server;

//Tony's Test

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import java.net.UnknownHostException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Hashtable;
import java.util.Set;

import common.Message;
import common.Parser;
import common.Reply;
import common.Transition;
import common.Enums.MessageType;
import common.Enums.ProtocolState;

/**
 * @author alex and leah
 * The chat server, which will accept connection from multiple clients.  The
 * server will be a middle man that sends messages between clients.  For each 
 * client, the server will maintain a new thread representing the protocol 
 * state for that server-client pair.
 */
public class ChatServer
{
    private static Hashtable<String, String> validUsers = null;
    
    private static Hashtable<Integer, Connection> connections = null;
    private static Hashtable<String, Integer> usernameToId = null;
    
    private static int nextId = 0;
    private static boolean ready = false;
    
    private static ChatServer ThisChatServer = new ChatServer();
    
    /**
     * The entry point for the chat server.
     * @param argv Command line arguments.
     * @throws UnknownHostException
     * @throws IOException
     * @throws InterruptedException 
     */
    public static void main(String argv[]) throws UnknownHostException, IOException, InterruptedException
    {
        // The code that actually sends data across the socket is partially 
        // taken from the lecture slides and this website:
        // http://systembash.com/content/a-simple-java-tcp-server-and-tcp-client/
        // This really only makes up a few lines of code, but we are noting 
        // this here just to provide a reference.
        
        // hard-code the list of valid users
        validUsers = new Hashtable<String, String>();
        validUsers.put("alex", "password1");
        validUsers.put("james", "password2");
        validUsers.put("leah", "password3");
        validUsers.put("tony", "password4");
        
        // create the chat server and initialize the hash tables
        connections = new Hashtable<Integer, Connection>();
        usernameToId = new Hashtable<String, Integer>();
        SSLServerSocket chatServer = (SSLServerSocket) SSLServerSocketFactory.getDefault().createServerSocket(6969);
        
        // wait until a connection is established
        System.out.println("Waiting for first connection...");
        
        
        while (true)
        {
            final SSLSocket connectionSocket = (SSLSocket) chatServer.accept();
            
            new Thread
            (
                new Runnable()
                {
                    private ProtocolState currentState = null;
                    private ProtocolState expectedState = null;
                    private String toUser = null;
                    
                    SSLSocket thisSocket = connectionSocket;
                    int thisId = nextId;
                    
                        
                    public void run()
                    {
                        currentState = ProtocolState.INITIAL;
                        ready = true;
                        
                        // increment the next id and put the connection into the set of connections
                        try
                        {
                            connections.put(nextId, (ThisChatServer).new Connection(connectionSocket));
                        }
                        catch (IOException e)
                        {
                        }
                        nextId++;
                        
                        SafePrint(thisId, "Connection established!" + "\n");
                        SafePrint(thisId, "Listening for client messages..." + "\n");
                        
                        // create the data streams from/to the server
                        BufferedReader inFromClient = null;
                        DataOutputStream outToClient = null;
                        
                        try
                        {
                            inFromClient = new BufferedReader(new InputStreamReader(thisSocket.getInputStream()));
                            outToClient = new DataOutputStream(thisSocket.getOutputStream());
                        }
                        catch (IOException e)
                        {
                        }
                        String firstMessage = "";
                        firstMessage += "Network connection established!  ";
                        firstMessage += "You are client " + thisId + ".  ";
                        firstMessage += "Send a JOIN message to start." + "\r\n";
                        WriteBytesToClient(outToClient, firstMessage);
                        String clientMessage = "";
                        currentState = ProtocolState.INITIAL;
                        
                        // continue looping until the client sends an "LOGOUT" message
                        while(currentState != ProtocolState.LOGOUT)
                        {
                            // get the message from the client and output it
                            try
                            {
                                clientMessage = inFromClient.readLine();
                            }
                            catch (IOException e)
                            {
                            }
                            SafePrint(thisId, "Received: " + clientMessage + "\n");
                                                       
                                
                            // tell the client that this message has been received
                            String type = Parser.GetNetworkMessageType(clientMessage);
                            if (type == null)
                            {
                                Reply r = new Reply(412, "Invalid message type received");
                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                            }
                            else if (type.equals("M"))
                            {
                                Message m = Parser.StringToMessage(clientMessage);
                                if (m != null)
                                {
                                    // When JOIN is accepted, AUTHENTICATE is sent to the client
                                    if (m.GetMessageType() == MessageType.JOIN)
                                    {
                                        currentState = ProtocolState.AUTHENTICATION;
                                        Message tempM = new Message(MessageType.AUTHENTICATE);
                                        tempM.SetParameter("auth_type", "username,password");
                                        WriteBytesToClient(outToClient, Parser.MessageToString(tempM) + "\r\n");    
                                    }
                                    // When LOGIN is accepted, send 102 message,
                                    // or 202 if LOGIN invalid,
                                    // or 413 if LOGIN is requested at the wrong time
                                    else if (m.GetMessageType() == MessageType.LOGIN)
                                    {
                                        if (currentState == ProtocolState.AUTHENTICATION || 
                                            currentState == ProtocolState.CONNECTED)
                                        {
                                            String username = m.GetParameter("username");
                                            String password = m.GetParameter("password");
                                            if (username != null && password != null &&
                                                !username.contains(" ") &&
                                                validUsers.containsKey(username) && 
                                                validUsers.get(username).equals(password) &&
                                                !usernameToId.containsKey(username)
                                                )
                                            {
                                                currentState = ProtocolState.CONNECTED;
                                                connections.get(thisId).username = username;
                                                Reply r = new Reply(102, "LOGIN accepted");
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                                
                                                usernameToId.put(username, thisId);
                                            }
                                            else
                                            {
                                                Reply r = new Reply(202, "LOGIN denied");
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                            }
                                        }
                                        else
                                        {
                                            Reply r = new Reply(413, "LOGIN message not appropriate here");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    // When LOGOUT is accepted, send 103 message.
                                    else if (m.GetMessageType() == MessageType.LOGOUT)
                                    {
                                        if (currentState == ProtocolState.CONNECTED || 
                                            currentState == ProtocolState.COMPOSITION ||
                                            currentState == ProtocolState.AUTHENTICATION ||
                                            currentState == ProtocolState.INITIAL)
                                        {
                                            currentState = ProtocolState.LOGOUT;
                                            Reply r = new Reply(103, "LOGOUT accepted");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    else if (m.GetMessageType() == MessageType.LIST)
                                    {
                                        if (currentState == ProtocolState.CONNECTED)
                                        {
                                            Set<String> users = usernameToId.keySet();
                                            String usersString = "";
                                            for (String user : users)
                                            {
                                                usersString += " " + user;
                                            }
                                            Reply r = new Reply(109, "User List" + usersString);
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    /* No longer need RESTART message
                                    // When RESTART is accepted, send 104 message.
                                    else if (m.GetMessageType() == MessageType.RESTART)
                                    {
                                        if (currentState == ProtocolState.AUTHENTICATION || 
                                            currentState == ProtocolState.CONNECTED || 
                                            currentState == ProtocolState.COMPOSITION ||
                                            currentState == ProtocolState.INITIAL)
                                        {
                                            Reply r = new Reply(104, "RESTART accepted");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    */
                                    // When SEND is accepted and server is ready for data,
                                    // send 105 message or 205 if unable to send to user.
                                    else if (m.GetMessageType() == MessageType.SEND)
                                    {
                                        if (currentState == ProtocolState.CONNECTED)
                                        {
                                            toUser = m.GetParameter("to");
                                            if (toUser != null && usernameToId.containsKey(toUser))
                                            {
                                                Reply r = new Reply(105, "SEND accepted. Ready for data.");
                                                currentState = ProtocolState.COMPOSITION;
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                            }
                                            else
                                            {
                                                Reply r = new Reply(205, "SEND failed. User does not exist or was not specified.");
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                            }
                                        }
                                        else
                                        {
                                            Reply r = new Reply(414, "SEND message not appropriate here");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    // When DATA is accepted and sent,
                                    // send 106 message or 206 if unable to send to user.
                                    else if (m.GetMessageType() == MessageType.DATA)
                                    {
                                        if (currentState == ProtocolState.COMPOSITION)
                                        {
                                            try
                                            {
                                                Reply r = new Reply(106, "DATA accepted. Message sent.");
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                                
                                                // get destination client ID and sending data to destination client
                                                int toId = usernameToId.get(toUser);
                                                String contents = "From " + connections.get(thisId).username + ": " + m.GetContent();
                                                WriteBytesToClient(connections.get(toId).outgoing, contents + "\r\n");
                                            }
                                            catch (Exception ex)
                                            {
                                                Reply r = new Reply(206, "DATA failed. Server unable to send.");
                                                WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                            }
                                        }
                                        else
                                        {
                                            Reply r = new Reply(415, "DATA message not appropriate here");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    // When CANCEL is accepted and message is canceled, 
                                    // send 107 message.
                                    else if (m.GetMessageType() == MessageType.CANCEL)
                                    {
                                        if (currentState == ProtocolState.COMPOSITION)
                                        {
                                            currentState = ProtocolState.CONNECTED;
                                            Reply r = new Reply(107, "Message CANCELED.");
                                            WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                                        }
                                    }
                                    
                                    // this just echoes the message back to the client
                                    //WriteBytesToClient(outToClient, "Received Message: " + Parser.MessageToString(m) + "\r\n");
                                }
                            }
                            else if (type.equals("R"))
                            {
                                Reply r = Parser.StringToReply(clientMessage);
                                // 101: Positive reply to AUTHENTICATE
                                if (r.GetId() == 101 && 
                                    expectedState == ProtocolState.AUTHENTICATION)
                                {
                                    if (Transition.TransitionCheck(currentState, expectedState))
                                        currentState = expectedState;
                                }
                                // 401: Negative reply to AUTHENICATE
                                else if (r.GetId() == 401)
                                {
                                    currentState = ProtocolState.LOGOUT;
                                }
                                // 108: Positive reply to INCOMING message
                                else if (r.GetId() == 108 &&
                                         (expectedState == ProtocolState.CONNECTED || 
                                          expectedState == ProtocolState.COMPOSITION))
                                {
                                    if (Transition.TransitionCheck(currentState, expectedState))
                                        currentState = expectedState;
                                }
                                // 408: Negative reply to INCOMING message
                                else if (r.GetId() == 408)
                                {
                                    currentState = ProtocolState.LOGOUT;
                                }
                                // 110: Positive ALIVE received
                                else if (r.GetId() == 110 &&
                                        (expectedState == ProtocolState.AUTHENTICATION || 
                                         expectedState == ProtocolState.CONNECTED || 
                                         expectedState == ProtocolState.COMPOSITION))
                                {
                                    if (Transition.TransitionCheck(currentState, expectedState))
                                        currentState = expectedState;
                                }
                                // 410: Negative ALIVE received 
                                else if (r.GetId() == 410)
                                {
                                    currentState = ProtocolState.LOGOUT;
                                }
                                // 111: Positive FAIL received
                                else if (r.GetId() == 111 &&
                                        (expectedState == ProtocolState.CONNECTED || 
                                         expectedState == ProtocolState.COMPOSITION))
                                {
                                    if (Transition.TransitionCheck(currentState, expectedState))
                                        currentState = expectedState;
                                }
                                // 411: Negative FAIL received 
                                else if (r.GetId() == 411)
                                {
                                    currentState = ProtocolState.LOGOUT;
                                }
                                
                                // this just echoes the reply back to the client
                                //WriteBytesToClient(outToClient, "Received Reply: " + Parser.ReplyToString(r) + "\r\n");
                            }
                        }
                        
                        SafePrint(thisId, "Connection closed!" + "\n");
                        connections.remove(thisSocket);
                        
                        // remove all users for this id
                        ArrayList<String> users = Collections.list(usernameToId.keys());
                        for (String user : users)
                        {
                            if (usernameToId.get(user) == thisId)
                            {
                                usernameToId.remove(user);
                            }
                        }
                        
                        // tell client that connection is closed
                        Reply r = new Reply(112, "LOGOUT successful.");
                        WriteBytesToClient(outToClient, Parser.ReplyToString(r) + "\r\n");
                        
                    }
                }
            ).start();
            

            // wait until the current thread has initialized the connection socket
            while(!ready)
            {
                try
                {
                    Thread.sleep(100);
                }
                catch (InterruptedException e)
                {
                }
            }
        }
    }
    
    /**
     * Allows a thread to safely print to the console without having its text 
     * intertwined with what another thread is writing.
     * @param id The id of the client
     * @param s The string to print
     */
    private static void SafePrint(int id, String s)
    {
        synchronized(connections)
        {
            DateFormat dateFormat = new SimpleDateFormat("(HH:mm:ss)");
            String time = dateFormat.format(new Date());
            System.out.print(time + " " + "Client " + id + ": " + s);
        }
    }
    
    /**
     * Writes the specified string to the specified data output stream.
     * This will send the specified string to the client.
     * @param stream The data output stream.
     * @param data The string to write to the stream.
     */
    private static void WriteBytesToClient(DataOutputStream stream, String data)
    {
        try
        {
            stream.writeBytes(data);
        }
        catch (IOException e)
        {
        }
    }
    
    private class Connection
    {
        private Connection(SSLSocket s) throws IOException
        {
            this.socket = s;
            outgoing = new DataOutputStream(this.socket.getOutputStream());
        }
        
        private SSLSocket socket = null;
        private DataOutputStream outgoing = null;
        private String username = null;
    }
}